/*

  xmunipack - display image

  Copyright © 2009-2011 F.Hroch (hroch@physics.muni.cz)

  This file is part of Munipack.

  Munipack is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
  
  Munipack is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  
  You should have received a copy of the GNU General Public License
  along with Munipack.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "xmunipack.h"
#include <wx/wx.h>
#include <wx/dcbuffer.h>
#include <wx/brush.h>
#include <wx/graphics.h>
#include <wx/renderer.h>
#include <wx/thread.h>
#include <wx/statline.h>
#include <wx/animate.h>
#include <wx/xml/xml.h>
#include <wx/mstream.h>
#include <wx/scrolwin.h>
#include <cfloat>

using namespace std;


// -- MuniDisplayPanel

class MuniDisplayPanel: public wxScrolledWindow
{
public:
  MuniDisplayPanel(wxWindow *w, MuniConfig *c);

  void SetHdu(const FitsArray&, const FitsHisto&);
  
private:

  MuniConfig *config;
  FitsValue fvalue,value;
  FitsCoo fcoords,coords;
  FitsArray hdu;
  wxString fval_str,val_str,fxcoo_str,fycoo_str,xcoo_str,ycoo_str,tobject,
    tdate,ttime,texp,tband,tsize;
  MuniCanvasMini *zoom;
  wxTextCtrl *tval,*val,*xcoo,*ycoo,*acoo,*dcoo;
  MuniPlotHisto *phisto;

  void OnPopVal(wxCommandEvent&);
  void OnPopCoo(wxCommandEvent&);
  void OnCooUnit(wxCommandEvent&);
  void OnValUnit(wxCommandEvent&);
  void OnMouseMotion(MuniSlewEvent&);
  void OnUpdateValue(wxUpdateUIEvent&);
  void OnUpdateFValue(wxUpdateUIEvent&);
  void OnUpdateXCoo(wxUpdateUIEvent&);
  void OnUpdateYCoo(wxUpdateUIEvent&);
  void OnUpdateACoo(wxUpdateUIEvent&);
  void OnUpdateDCoo(wxUpdateUIEvent&);
  /*
  void OnUpdateObject(wxUpdateUIEvent&);
  void OnUpdateDate(wxUpdateUIEvent&);
  void OnUpdateTime(wxUpdateUIEvent&);
  void OnUpdateExp(wxUpdateUIEvent&);
  void OnUpdateBand(wxUpdateUIEvent&);
  void OnUpdateSize(wxUpdateUIEvent&);
  */
};

MuniDisplayPanel::MuniDisplayPanel(wxWindow *w, MuniConfig *c):
  wxScrolledWindow(w,wxID_ANY,wxDefaultPosition,wxDefaultSize,wxTAB_TRAVERSAL|wxBORDER_THEME),
  config(c)
{
  SetScrollRate(-1,1);
  
  wxColour bc(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWFRAME));

  wxFont bf(*wxNORMAL_FONT);
  bf.SetWeight(wxFONTWEIGHT_BOLD);

  const long DefaultStyle = wxTE_RIGHT|wxTE_DONTWRAP|wxTE_READONLY|wxBORDER_NONE;

  // detail
  zoom = new MuniCanvasMini(this,config->detail_zoom,config->detail_scale);

  //  wxArrayString sdetail;
  //  sdetail.Add(wxT("Image"));
  //  sdetail.Add(wxT("Profile 2D"));
  //  sdetail.Add(wxT("Profile 3D"));
  //  sdetail.Add(wxT("Contours"));

  // wxChoice *dtvalue = new wxChoice(this,wxID_ANY,wxDefaultPosition,
  // 				   wxDefaultSize,sdetail);  
  // wxStaticBoxSizer *zsizer = new wxStaticBoxSizer(wxVERTICAL,this);
  // zsizer->Add(zoom,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL|wxALIGN_CENTER_HORIZONTAL).Border(wxTOP|wxBOTTOM));
  // zsizer->Add(dtvalue,wxSizerFlags().Expand().Align(wxALIGN_BOTTOM));

  // value
  wxFlexGridSizer *cgrid = new wxFlexGridSizer(3);
  cgrid->AddGrowableCol(1);


  wxStaticText *lint = new wxStaticText(this,wxID_ANY,"I:");
  lint->SetFont(bf);
  cgrid->Add(lint,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  val = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
		       wxDefaultSize, DefaultStyle);
  val->SetBackgroundColour(bc);
  val->SetToolTip("Intensity in selected physical units");
  cgrid->Add(val,wxSizerFlags(1).Expand());

  wxButton *inttype = new wxButton(this,wxID_ANY,"»",wxDefaultPosition, 
				   wxDefaultSize,wxBU_EXACTFIT);
  cgrid->Add(inttype,wxSizerFlags());

  wxStaticText *lval = new wxStaticText(this,wxID_ANY,"c:");
  lval->SetFont(bf);
  cgrid->Add(lval,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  tval = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, DefaultStyle);
  tval->SetBackgroundColour(bc);
  tval->SetToolTip("Any value. An instrumental intensity in counts (ADU)");
  cgrid->Add(tval,wxSizerFlags().Expand());
  cgrid->Add(1,1);


  // coordinates
  /*
  wxBoxSizer *xysizer = new wxBoxSizer(wxHORIZONTAL);

  wxStaticText *lx = new wxStaticText(this,wxID_ANY,"x:");
  lx->SetFont(bf);
  xysizer->Add(lx,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  xcoo = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, wxTE_RIGHT|wxTE_DONTWRAP|wxTE_READONLY);
  xcoo->SetBackgroundColour(bc);
  xcoo->SetToolTip("Rectangular horizontal coordinate in pixels. Origin at left bottom corner.");
  xysizer->Add(xcoo,wxSizerFlags());

  wxStaticText *ly = new wxStaticText(this,wxID_ANY,"y:");
  ly->SetFont(bf);
  xysizer->Add(ly,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL).Border(wxLEFT));
  ycoo = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, wxTE_RIGHT|wxTE_DONTWRAP|wxTE_READONLY);
  ycoo->SetBackgroundColour(bc);
  ycoo->SetToolTip("Rectangular vertical coordinate in pixels. Origin at left bottom corner.");
  xysizer->Add(ycoo,wxSizerFlags());
  */

  wxFlexGridSizer *xgrid = new wxFlexGridSizer(3);
  xgrid->AddGrowableCol(1);

  wxStaticText *la = new wxStaticText(this,wxID_ANY,"α:");
  la->SetFont(bf);
  xgrid->Add(la,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  acoo = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, DefaultStyle);
  acoo->SetBackgroundColour(bc);
  acoo->SetToolTip("Spherical coordinate - longitude");
  xgrid->Add(acoo,wxSizerFlags().Expand());

  wxButton *ctype = new wxButton(this,wxID_ANY,"»",wxDefaultPosition, 
				 wxDefaultSize,wxBU_EXACTFIT);
  xgrid->Add(ctype,wxSizerFlags());

  wxStaticText *ld = new wxStaticText(this,wxID_ANY,"δ:");
  ld->SetFont(bf);
  xgrid->Add(ld,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  dcoo = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, DefaultStyle);
  dcoo->SetBackgroundColour(bc);
  dcoo->SetToolTip("Spherical coordinate - latitude");
  xgrid->Add(dcoo,wxSizerFlags().Expand());
  xgrid->Add(1,1);

  wxStaticText *lx = new wxStaticText(this,wxID_ANY,"x:");
  lx->SetFont(bf);
  xgrid->Add(lx,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  xcoo = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, DefaultStyle);
  xcoo->SetBackgroundColour(bc);
  xcoo->SetToolTip("Rectangular horizontal coordinate in pixels. Origin at left bottom corner.");
  xgrid->Add(xcoo,wxSizerFlags().Expand());
  xgrid->Add(1,1);

  wxStaticText *ly = new wxStaticText(this,wxID_ANY,"y:");
  ly->SetFont(bf);
  xgrid->Add(ly,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  ycoo = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, DefaultStyle);
  ycoo->SetBackgroundColour(bc);
  ycoo->SetToolTip("Rectangular vertical coordinate in pixels. Origin at left bottom corner.");
  xgrid->Add(ycoo,wxSizerFlags().Expand());
  xgrid->Add(1,1);


  // info
  /*
  wxFlexGridSizer *igrid = new wxFlexGridSizer(2);
  igrid->AddGrowableCol(1);

  wxStaticText *lobject = new wxStaticText(this,wxID_ANY,"Object:");
  lobject->SetFont(bf);
  igrid->Add(lobject,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  wxTextCtrl *iobj = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
					wxDefaultSize, wxTE_RIGHT|wxTE_DONTWRAP|wxTE_READONLY);
  iobj->SetBackgroundColour(bc);
  iobj->SetToolTip("Target object name");
  igrid->Add(iobj,wxSizerFlags().Expand());
  
  wxStaticText *ldate = new wxStaticText(this,wxID_ANY,"Date:");
  ldate->SetFont(bf);
  igrid->Add(ldate,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL|wxRIGHT));
  wxTextCtrl *idate = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
				     wxDefaultSize, wxTE_RIGHT|wxTE_DONTWRAP|wxTE_READONLY);
  idate->SetBackgroundColour(bc);
  idate->SetToolTip("Date of exposure start in UT");
  igrid->Add(idate,wxSizerFlags().Expand());

  wxStaticText *ltime = new wxStaticText(this,wxID_ANY,"Time:");
  ltime->SetFont(bf);
  igrid->Add(ltime,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  wxTextCtrl *itime = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, wxTE_RIGHT|wxTE_DONTWRAP|wxTE_READONLY);
  itime->SetBackgroundColour(bc);
  itime->SetToolTip("Time of exposure start in UT");
  igrid->Add(itime,wxSizerFlags().Expand());

  wxStaticText *lexp = new wxStaticText(this,wxID_ANY,"Etime:");
  lexp->SetFont(bf);
  igrid->Add(lexp,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  wxTextCtrl *iexp = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, wxTE_RIGHT|wxTE_DONTWRAP|wxTE_READONLY);
  iexp->SetToolTip("Exposure time in seconds");
  iexp->SetBackgroundColour(bc);
  igrid->Add(iexp,wxSizerFlags().Expand());

  wxStaticText *lband = new wxStaticText(this,wxID_ANY,"Band:");
  lband->SetFont(bf);
  igrid->Add(lband,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  wxTextCtrl *iband = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, wxTE_RIGHT|wxTE_DONTWRAP|wxTE_READONLY);
  iband->SetBackgroundColour(bc);
  iband->SetToolTip("Spectral band description");
  igrid->Add(iband,wxSizerFlags().Expand());

  wxStaticText *lsize = new wxStaticText(this,wxID_ANY,"Size:");
  lsize->SetFont(bf);
  igrid->Add(lsize,wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL));
  wxTextCtrl *isize = new wxTextCtrl(this, wxID_ANY, wxEmptyString,wxDefaultPosition, 
			wxDefaultSize, wxTE_RIGHT|wxTE_DONTWRAP|wxTE_READONLY);
  isize->SetBackgroundColour(bc);

  igrid->Add(isize,wxSizerFlags().Expand());
  isize->SetToolTip("Dimensions (width × height × ..) of image in pixels");
  */

  phisto = new MuniPlotHisto(this,false);


  // top sizer
  wxBoxSizer *topsizer = new wxBoxSizer(wxVERTICAL);
  topsizer->Add(zoom,wxSizerFlags().Center().Border());
  topsizer->Add(new wxStaticLine(this,wxID_ANY),wxSizerFlags().Expand().Border(wxLEFT|wxRIGHT));
  topsizer->Add(cgrid,wxSizerFlags().Expand().Border());
  topsizer->Add(new wxStaticLine(this,wxID_ANY),wxSizerFlags().Expand().Border(wxLEFT|wxRIGHT));
  topsizer->Add(xgrid,wxSizerFlags().Expand().Border());
  //  topsizer->Add(xysizer,wxSizerFlags().Expand());
  /*
  topsizer->Add(new wxStaticLine(this,wxID_ANY),wxSizerFlags().Expand().Border());
  topsizer->Add(igrid,wxSizerFlags().Expand());  
  */
  topsizer->Add(new wxStaticLine(this,wxID_ANY),wxSizerFlags().Expand().Border(wxLEFT|wxRIGHT));
  topsizer->Add(phisto,wxSizerFlags().Expand().Border());
  SetSizer(topsizer);

  // bindings
  Bind(wxEVT_COMMAND_BUTTON_CLICKED,&MuniDisplayPanel::OnPopVal,this,inttype->GetId());
  Bind(wxEVT_COMMAND_BUTTON_CLICKED,&MuniDisplayPanel::OnPopCoo,this,ctype->GetId());
  Bind(xEVT_SLEW,&MuniDisplayPanel::OnMouseMotion,this);
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateValue,this,tval->GetId());
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateFValue,this,val->GetId());  
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateXCoo,this,xcoo->GetId());
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateYCoo,this,ycoo->GetId());
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateACoo,this,acoo->GetId());
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateDCoo,this,dcoo->GetId());
  /*
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateObject,this,iobj->GetId());
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateDate,this,idate->GetId());
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateTime,this,itime->GetId());
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateExp,this,iexp->GetId());
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateBand,this,iband->GetId());
  Bind(wxEVT_UPDATE_UI,&MuniDisplayPanel::OnUpdateSize,this,isize->GetId());
  */
  Bind(wxEVT_COMMAND_MENU_SELECTED,&MuniDisplayPanel::OnCooUnit,this,ID_COO_EQDEG);
  Bind(wxEVT_COMMAND_MENU_SELECTED,&MuniDisplayPanel::OnCooUnit,this,ID_COO_EQSIX);
  Bind(wxEVT_COMMAND_MENU_SELECTED,&MuniDisplayPanel::OnValUnit,this,ID_UNIT_MAG);
  Bind(wxEVT_COMMAND_MENU_SELECTED,&MuniDisplayPanel::OnValUnit,this,ID_UNIT_PHOTON);
  Bind(wxEVT_COMMAND_MENU_SELECTED,&MuniDisplayPanel::OnValUnit,this,ID_UNIT_ERG);
}

void MuniDisplayPanel::SetHdu(const FitsArray& h, const FitsHisto& hist)
{
  hdu = h;
  value = FitsValue(hdu);
  fvalue = FitsValue(hdu);
  coords = FitsCoo(hdu);
  fcoords = FitsCoo(hdu);

  fcoords.SetType(COO_EQSIX);
  fvalue.SetType(UNIT_MAG);

  tobject = hdu.GetKey(config->fits_object);
  tband = hdu.GetKey(config->fits_filter);
  texp = hdu.GetKey(config->fits_exposure);

  wxString dateobs;
  dateobs = hdu.GetKey(config->fits_dateobs);
  FitsTime ft(dateobs);
  tdate = ft.Date();
  ttime = ft.Time();

  // dimensions
  wxString a;
  a.Printf(wxT("%d"),(int) hdu.Naxes(0));
  for(int k = 1; k < hdu.Naxis(); k++) {
    wxString b;
    b.Printf(wxT(" × %d"),(int) hdu.Naxes(k));
    a += b;
  }
  tsize = a;


  // histogram
  phisto->SetHisto(hist);
}

void MuniDisplayPanel::OnPopVal(wxCommandEvent& event)
{
  wxMenu popup;
  popup.AppendRadioItem(ID_UNIT_MAG,FitsValue::Label_str(UNIT_MAG));
  popup.AppendRadioItem(ID_UNIT_PHOTON,FitsValue::Label_str(UNIT_PHOTON));
  popup.AppendRadioItem(ID_UNIT_ERG,FitsValue::Label_str(UNIT_ERG));

  switch(fvalue.GetType()) {
  case UNIT_MAG:    popup.Check(ID_UNIT_MAG,true);    break;
  case UNIT_PHOTON: popup.Check(ID_UNIT_PHOTON,true); break;
  case UNIT_ERG:    popup.Check(ID_UNIT_ERG,true);    break;
  default: wxFAIL_MSG("----- WARNING: Unreachable code.");
  }

  PopupMenu(&popup);
}

void MuniDisplayPanel::OnPopCoo(wxCommandEvent& event)
{
  wxMenu popup;
  popup.AppendRadioItem(ID_COO_EQSIX,FitsCoo::Label_str(COO_EQSIX));
  popup.AppendRadioItem(ID_COO_EQDEG,FitsCoo::Label_str(COO_EQDEG));

  switch(fcoords.GetType()) {
  case COO_EQSIX: popup.Check(ID_COO_EQSIX,true); break;
  case COO_EQDEG: popup.Check(ID_COO_EQDEG,true); break;
  default: wxFAIL_MSG("----- WARNING: Unreachable code.");
  }

  //  Bind(wxEVT_COMMAND_MENU_SELECTED,&MuniDisplayPanel::OnCooUnit,this,ID_COO_EQDEG);
  //  Bind(wxEVT_COMMAND_MENU_SELECTED,&MuniDisplayPanel::OnCooUnit,this,ID_COO_EQSIX);

  PopupMenu(&popup);

  //  Unbind(wxEVT_COMMAND_MENU_SELECTED,&MuniDisplayPanel::OnCooUnit,this,ID_COO_EQDEG);
  //  Unbind(wxEVT_COMMAND_MENU_SELECTED,&MuniDisplayPanel::OnCooUnit,this,ID_COO_EQSIX);

}

void MuniDisplayPanel::OnCooUnit(wxCommandEvent& event)
{
  int t = 0;
  switch(event.GetId()){
  case ID_COO_EQDEG:    fcoords.SetType(COO_EQDEG); t = COO_EQDEG; return;
  case ID_COO_EQSIX:    fcoords.SetType(COO_EQSIX); t = COO_EQSIX; return;
  }
  wxLogDebug(FitsCoo::Label_str(t));
}

void MuniDisplayPanel::OnValUnit(wxCommandEvent& event)
{
  int t = 0;

  switch(event.GetId()){
  case ID_UNIT_MAG:    fvalue.SetType(UNIT_MAG);    t = UNIT_MAG;    return;
  case ID_UNIT_PHOTON: fvalue.SetType(UNIT_PHOTON); t = UNIT_PHOTON; return;
  case ID_UNIT_ERG:    fvalue.SetType(UNIT_ERG);    t = UNIT_ERG;    return;
  }
  
  val->SetToolTip("Intensity in selected physical units: " +
		  FitsValue::Label_str(t));
  wxLogDebug(FitsValue::Label_str(t));

}

void MuniDisplayPanel::OnMouseMotion(MuniSlewEvent& event)
{
  int x = event.i_x;
  int y = event.i_y;

  // convert to a text string (used by *Update*)
  val_str = value.Get_str(x,y);
  fval_str = fvalue.Get_str(x,y);

  coords.GetStr(x,y,xcoo_str,ycoo_str);
  fcoords.GetStr(x,y,fxcoo_str,fycoo_str);

  // image
  FitsBitmap picture(event.picture);
  if( !picture.IsOk()) return;
  int z = config->detail_scale;
  wxImage i(picture.GetWidth(),picture.GetHeight(),picture.NewTopsyTurvyRGB());
  if( i.IsOk() ) {
    i.Rescale(z*i.GetWidth(),z*i.GetHeight());
    zoom->SetImage(wxBitmap(i));
  }
}

void MuniDisplayPanel::OnUpdateValue(wxUpdateUIEvent& event)
{
  event.SetText(val_str);
}

void MuniDisplayPanel::OnUpdateFValue(wxUpdateUIEvent& event)
{
  event.SetText(fval_str);
}

void MuniDisplayPanel::OnUpdateXCoo(wxUpdateUIEvent& event)
{
  event.SetText(xcoo_str);
}

void MuniDisplayPanel::OnUpdateYCoo(wxUpdateUIEvent& event)
{
  event.SetText(ycoo_str);
}

void MuniDisplayPanel::OnUpdateACoo(wxUpdateUIEvent& event)
{
  event.SetText(fxcoo_str);
}

void MuniDisplayPanel::OnUpdateDCoo(wxUpdateUIEvent& event)
{
  event.SetText(fycoo_str);
}

/*
void MuniDisplayPanel::OnUpdateObject(wxUpdateUIEvent& event)
{
  event.SetText(tobject);
}

void MuniDisplayPanel::OnUpdateDate(wxUpdateUIEvent& event)
{
  event.SetText(tdate);
}

void MuniDisplayPanel::OnUpdateTime(wxUpdateUIEvent& event)
{
  event.SetText(ttime);
}

void MuniDisplayPanel::OnUpdateExp(wxUpdateUIEvent& event)
{
  event.SetText(texp);
}

void MuniDisplayPanel::OnUpdateBand(wxUpdateUIEvent& event)
{
  event.SetText(tband);
}

void MuniDisplayPanel::OnUpdateSize(wxUpdateUIEvent& event)
{
  event.SetText(tsize);
}
*/

// -- MuniDisplayCanvas


class MuniDisplayCanvas: public wxScrolledCanvas
{
public:
  MuniDisplayCanvas(wxWindow *w, MuniConfig *c);
  virtual ~MuniDisplayCanvas();
  
  wxSize DoGetBestSize() const;
  void SetHandler(wxEvtHandler *);
  void SetHdu(const FitsArray&, const wxImage&, const FitsArrayStat&);
  void Render();
  void StopRendering();
  bool IsRendering() const { return render; }
  double GetFitZoom() const;
  double GetZoom() const { return zoom; }
  FitsItt GetItt() const { return itt; };
  FitsPalette GetPalette() const { return pal; }
  FitsColor GetColor() const { return color; }
  wxImage GetImage() { return bitmap.ConvertToImage(); }
  bool GetFit() const { return fit; }

  void OnTuneFine(MuniTuneEvent&);
  void OnAstrometry(MuniAstrometryEvent&);
  void OnConfigUpdated(wxCommandEvent&);
  //  void AddOverlay(const vector<MuniDrawBase *>& o) { /*drawobj = o;*/ OverlayBitmap(o); Refresh();}
  //  void ShowGrid(const vector<wxGraphicsPath>& ps) { OverlayGrid(ps); Refresh();}

  //protected:

  wxCriticalSection renderCS;
  ImageRender *render;

private:

  wxWindow *topwin;
  MuniConfig *config;
  wxBitmap bitmap,dcbitmap;
  wxMemoryDC memdc;
  vector<wxBitmap> overlay;
  wxEvtHandler *handler;

  FitsArray hdu;
  wxImage icon;
  int xoff,yoff,xdrag0,ydrag0;
  FitsImage fitsimage, fimage;
  FitsDisplay display;
  FitsBitmap picture;
  FitsPalette pal;
  FitsItt itt, itt_orig;
  FitsColor color;
  double zoom,scale;
  bool zooming;
  bool dragging,rendering;
  bool stick,fit,astrometry;
  double xcen, ycen, acen, dcen, ascale, aangle;
  //  vector<MuniDrawBase *> drawobj;
  //  std::vector<wxXmlDocument> layers;
  vector<MuniLayer> layers;

  void OnSize(wxSizeEvent&);
  void OnPaint(wxPaintEvent&);
  void OnMouseMotion(wxMouseEvent&);
  void OnMouseWheel(wxMouseEvent&);
  void OnLeftUp(wxMouseEvent&);
  void OnRenderFinish(MuniRenderEvent&);
  void OnSubRender(MuniRenderEvent&);
  void InitDC(int,int,const wxBitmap&);
  void Reset();
  void UpdateBitmap(int, int, const wxBitmap&);
  //  void OverlayBitmap(const vector<MuniDrawBase *>&);
  //  void OverlayGrid(const vector<wxGraphicsPath>&);
  void SetInitZoom(double =-1.0);
  void OnStick(wxCommandEvent&);
  void OnKeyDown(wxKeyEvent&);
  void OnMenu(wxMouseEvent& WXUNUSED(event));
  void OnIdle(wxIdleEvent&);
  void InitITT(const FitsArray&, const FitsArrayStat&);
  //  void DrawSVG(wxPaintDC&, const wxXmlDocument&);
  void DrawLayer(wxPaintDC&, const MuniLayer&);

};

MuniDisplayCanvas::MuniDisplayCanvas(wxWindow *w, MuniConfig *c):
  wxScrolledCanvas(w,wxID_ANY),render(0),topwin(GetGrandParent()),config(c),handler(0),
  xoff(0), yoff(0),
  display(config->display_colorspace),
  zoom(0.0),scale(1.0),zooming(true),dragging(false),rendering(false),fit(false),astrometry(false)
{
  SetBackgroundStyle(wxBG_STYLE_PAINT);

  pal.SetPalette(config->display_pal);
  itt.SetItt(config->display_itt);
  display.SetTemperature(config->color_temperature);
  display.SetColorspace(config->display_colorspace);
  color.SetTrans("XYZ",config->cdatafile);
  color.SetWhitePoint(config->color_uwhitepoint,config->color_vwhitepoint);

  Bind(wxEVT_SIZE,&MuniDisplayCanvas::OnSize,this);
  Bind(wxEVT_PAINT,&MuniDisplayCanvas::OnPaint,this);
  Bind(wxEVT_IDLE,&MuniDisplayCanvas::OnIdle,this);
  Bind(EVT_RENDER,&MuniDisplayCanvas::OnRenderFinish,this,ID_RENDER);
  Bind(EVT_RENDER,&MuniDisplayCanvas::OnSubRender,this,ID_SUBRENDER);
  Bind(EVT_CONFIG_UPDATED,&MuniDisplayCanvas::OnConfigUpdated,this);
  Bind(wxEVT_KEY_DOWN,&MuniDisplayCanvas::OnKeyDown,this);
  Bind(wxEVT_RIGHT_DOWN,&MuniDisplayCanvas::OnMenu,this);
}

MuniDisplayCanvas::~MuniDisplayCanvas()
{
  /*
  vector<MuniDrawBase *>::const_iterator i;
  for(i = drawobj.begin(); i != drawobj.end(); ++i)
    delete *i;
  */

  StopRendering();

  config->display_itt = itt.GetItt();
  config->display_pal = pal.GetPalette();
  config->display_zoom = zoom;

}

wxSize MuniDisplayCanvas::DoGetBestSize() const
{
  return wxSize(162,100);
}

void MuniDisplayCanvas::SetHandler(wxEvtHandler *h)
{
  handler = h;

  if( handler ) {
    Bind(wxEVT_MOTION,&MuniDisplayCanvas::OnMouseMotion,this);
    Bind(wxEVT_MOUSEWHEEL,&MuniDisplayCanvas::OnMouseWheel,this);
    Bind(wxEVT_LEFT_UP,&MuniDisplayCanvas::OnLeftUp,this);
  }
  else {
    Unbind(wxEVT_MOTION,&MuniDisplayCanvas::OnMouseMotion,this);
    Unbind(wxEVT_MOUSEWHEEL,&MuniDisplayCanvas::OnMouseWheel,this);
    Unbind(wxEVT_LEFT_UP,&MuniDisplayCanvas::OnLeftUp,this);
  }
}


void MuniDisplayCanvas::InitITT(const FitsArray& a, const FitsArrayStat& stat)
{
  if( a.IsColor() ) {
    itt.Init(stat.Med(),stat.Mad());
    itt.SetKind(ITT_KIND_ABS);
    itt.SetBlack(0.0);
    itt.SetContrast(6.5e4);
    color.SetTrans(hdu.GetKey("CSPACE"),config->cdatafile);

    for(int i = 0; i < a.Naxes(2); i++) {
      FitsArrayStat s(a.Plane(i));
      color.SetLevel(i,s.Med());
    }

  }
  else {
    itt.Init(stat.Med(),stat.Mad());
    itt.SetKind(ITT_KIND_REL);
  }

  itt_orig = itt;
}

void MuniDisplayCanvas::SetHdu(const FitsArray& a, const wxImage& i, const FitsArrayStat& stat)
{
  hdu = a;
  icon = i;

  wxASSERT(hdu.IsOk() && icon.IsOk());

  Reset();

  InitITT(a,stat);

  fit = config->display_zoom <= 0.0;
  SetInitZoom(config->display_zoom ? -1.0 : 1.0);

  int w = zoom*hdu.Width();
  int h = zoom*hdu.Height();
  if( w < 1 ) w = 1;
  if( h < 1 ) h = 1;

  bitmap = wxBitmap(icon.Scale(w,h));

  int wc,hc;
  GetClientSize(&wc,&hc);

  w = wxMax(w,wc);
  h = wxMax(h,hc);

  SetVirtualSize(w,h);
  InitDC(w,h,bitmap);

  SetScrollRate(1,1);

  fitsimage = FitsImage(hdu);
  wxASSERT(fitsimage.IsOk());

  fimage = fitsimage;


  Refresh();
  rendering = true;

  /*  */

  /*
  MuniLayer l;
  l.objects.push_back(new MuniDrawCircle(382.5,255.0,382.5));
  layers.push_back(l);
  */
}

void MuniDisplayCanvas::InitDC(int w, int h, const wxBitmap& bitmap)
{
  if( bitmap.IsOk() ) {
    wxSize canvas = GetClientSize();
    xoff = (canvas.GetWidth() - bitmap.GetWidth())/2;
    yoff = (canvas.GetHeight() - bitmap.GetHeight())/2;

    if( xoff < 0 ) xoff = 0;
    if( yoff < 0 ) yoff = 0;
  }

  if( scale <= 1.0 ) {

    dcbitmap = wxBitmap(w,h);
    memdc.SelectObjectAsSource(dcbitmap);
    if( memdc.IsOk() ) { 
      memdc.SetBackground(wxBrush(*wxBLACK));
      memdc.Clear();
      
      if( bitmap.IsOk() )
	memdc.DrawBitmap(bitmap,xoff,yoff,false);
    }
  }
}


void MuniDisplayCanvas::OnIdle(wxIdleEvent& event)
{
  // Rendering is started by Idle event FOLLOWING all (resize) events
  // The right canvas size is prepared for drawing.

  if( rendering ) {
    rendering = false;
    Render();
  }
}


void MuniDisplayCanvas::Reset()
{
  itt.SetItt(config->display_itt);
  itt = itt_orig;
  pal.SetPalette(config->display_pal);
  pal.SetNegative(false);
  color.SetSaturation(1.0);
  color.SetHue(0.0);
  color.SetNightVision(config->display_nvision);
  color.SetWhitePoint(config->color_uwhitepoint,config->color_vwhitepoint);
  color.SetNightThresh(0.0);
  color.SetNightWidth(30.0); // must correspods to initializer of Color?

  SetInitZoom(config->display_zoom ? -1.0 : 1.0);

  rendering = true;
}

void MuniDisplayCanvas::Render()
{
  wxASSERT(fitsimage.IsOk());
  //  wxLogDebug("MuniDisplayCanvas::Render");

  //stop when we are already rendering
  StopRendering();

  // render image
  display.SetItt(itt);
  display.SetPalette(pal);
  display.SetColor(color);

  /*
  const vector<FitsArray> band = fitsimage.GetArrays();
  for(size_t i = 0; i < band.size(); i++) {
    if( band[i].IsOk() )
      wxLogDebug("%f %f %f",band[i].Pixel(1206,1276),band[i].Pixel(1211,1284),band[i].Pixel(911,614));
  }
  */

  wxASSERT(render == 0);
  render = new ImageRender(this,fitsimage,fimage,display,zoom,zooming);
  wxThreadError code = render->Create();
  wxASSERT(code == wxTHREAD_NO_ERROR);
  render->Run();

  ::wxBeginBusyCursor();
  Refresh();
}


void MuniDisplayCanvas::OnSubRender(MuniRenderEvent& event)
{
  //  wxLogDebug("MuniDisplay::OnSubRender");

  picture = event.picture;
  wxASSERT(picture.IsOk());

  wxImage img(picture.GetWidth(),picture.GetHeight(),picture.NewTopsyTurvyRGB());

  UpdateBitmap(event.x,event.y,wxBitmap(img));
  Refresh();
}

void MuniDisplayCanvas::OnRenderFinish(MuniRenderEvent& event)
{
  //  wxLogDebug("MuniDisplay::OnRenderFinish");

  fimage = event.image;
  zooming = false;

  // if ! fimage.IsOk() ?????

  //  OverlayBitmap();

  Refresh();

  wxEndBusyCursor();
}

void MuniDisplayCanvas::StopRendering()
{
  //  wxLogDebug("MuniDisplayCanvas::StopRendering()");

  {
    wxCriticalSectionLocker enter(renderCS);
    if( render )
      render->Delete();
  }

  while(true) {
    {
      wxCriticalSectionLocker enter(renderCS);
      if( ! render ) break;
    }
    ::wxMilliSleep(1);
  }
}

void MuniDisplayCanvas::OnSize(wxSizeEvent& event)
{
  if( bitmap.IsOk() ) {

    int w = bitmap.GetWidth();
    int h = bitmap.GetHeight();

    int wc,hc;
    GetClientSize(&wc,&hc);

    w = wxMax(w,wc);
    h = wxMax(h,hc);

    SetVirtualSize(w,h);
    InitDC(w,h,bitmap);
  }

  Refresh();
  event.Skip();
}

void MuniDisplayCanvas::OnPaint(wxPaintEvent& event)
{
  wxPaintDC dc(this);
  DoPrepareDC(dc);

  int x,y,w,h;
  GetViewStart(&x,&y);
  GetClientSize(&w,&h);

  if( scale <= 1.0 )

    dc.Blit(x,y,w,h,&memdc,x,y);

  else {
    
    int x0 = x / scale;
    int y0 = y / scale;
    int wz = w / scale;
    int hz = h / scale;

    wxRect rect(0,0,bitmap.GetWidth(),bitmap.GetHeight());
    wxRect r = rect.Intersect(wxRect(x0,y0,wz,hz));

    wxBitmap b(bitmap.GetSubBitmap(r));
    wxImage i(b.ConvertToImage());
    i.Rescale(w,h,wxIMAGE_QUALITY_NEAREST);
    wxBitmap bmp(i);
    dc.DrawBitmap(bmp,x,y);

    //    dc.StretchBlit(x,y,w,h,&memdc,x,y,wz,hz); -- under GTK unimplemented yet

  }

  /*
  for(size_t i = 0; i < layers.size(); i++)
    DrawSVG(dc,layers[i]);
  */

  for(size_t i = 0; i < layers.size(); i++)
    DrawLayer(dc,layers[i]);

  // if( false /*astrometry*/ ) {

  //   dc.SetPen(*wxYELLOW_PEN);
  //   dc.SetBrush(*wxTRANSPARENT_BRUSH);

  //   const int ltic = 10;

  //   // central cross
  //   dc.DrawLine(xcen-ltic,ycen,xcen+ltic,ycen);
  //   dc.DrawLine(xcen,ycen-ltic,xcen,ycen+ltic);

  //   const int rrose = 137;
  //   const double rad = 57.3;

  //   // compass rose
  //   for(int i = 0; i < 36; i++) {
  //     double f = 10.0*i;
  //     double r1 = rrose;
  //     double r2 = rrose + ltic;
  //     double x1 = xcen + r1*cos(f/rad);
  //     double y1 = ycen + r1*sin(f/rad);
  //     double x2 = xcen + r2*cos(f/rad);
  //     double y2 = ycen + r2*sin(f/rad);
  //     dc.DrawLine(x1,y1,x2,y2);
  //   }

  //   // big cross
  //   for(int i = 0; i < 4; i++) {
  //     double f = 90.0*i + aangle;
  //     double r1 = 3.0*ltic;
  //     double r2 = rrose - 2.0*ltic;
  //     double x1 = xcen + r1*cos(f/rad);
  //     double y1 = ycen + r1*sin(f/rad);
  //     double x2 = xcen + r2*cos(f/rad);
  //     double y2 = ycen + r2*sin(f/rad);
  //     dc.DrawLine(x1,y1,x2,y2);
  //   }


  //   /*
  //   double r = 3600.0*ascale;
  //   dc.DrawCircle(xcen,ycen,r);
  //   double x = 10.0*r*sin(aangle/57.3);
  //   double y = 10.0*r*cos(aangle/57.3);
  //   dc.DrawLine(xcen,ycen,xcen+x,ycen+y);
  //   */
    
  // }

}

void MuniDisplayCanvas::DrawLayer(wxPaintDC& dc, const MuniLayer& layer)
{
  //  wxStopWatch sw;

  int height = bitmap.GetHeight();

  //  wxLogDebug("%d",(int)layer.objects.size());

  //  wxColour blue(wxBLUE->Red(),wxBLUE->Green(),wxBLUE->Blue(),50);

  /*
  dc.SetBrush(wxBrush(blue));
  dc.SetPen(wxPen(*wxYELLOW,1));

  for(size_t i = 0; i < layer.objects.size(); i++) {

    vector<wxObject *>::const_iterator i;
    for(i = layer.objects.begin(); i != layer.objects.end(); ++i) {

      MuniDrawCircle *circle = dynamic_cast<MuniDrawCircle *>(*i);
      if( circle ) {
	float d = circle->r*zoom;
	float x = xoff + zoom*circle->x;
	float y = yoff + (height - zoom*circle->y);
	dc.DrawCircle(x,y,d);
      }
	
      MuniDrawLine *line = dynamic_cast<MuniDrawLine *>(*i);
      if( line ) {
	wxPoint2DDouble points[2];
	points[0] = wxPoint2DDouble(zoom*line->x1,height-zoom*line->y1);
	points[1] = wxPoint2DDouble(zoom*line->x2,height-zoom*line->y2);
	dc.DrawLine(zoom*line->x1,height-zoom*line->y1,
		    zoom*line->x2,height-zoom*line->y2);
      }

      MuniDrawText *text = dynamic_cast<MuniDrawText *>(*i);
      if( text ) {
	dc.DrawText(text->text,zoom*text->x,height-zoom*text->y);
      }	
    }

  }
  */

  
  wxGraphicsContext *gc = wxGraphicsContext::Create(dc);
  if( gc ) {
    gc->SetAntialiasMode(wxANTIALIAS_NONE);
    gc->BeginLayer(0.42);

    //    gc->SetBrush(wxBrush(blue));
    gc->SetBrush(*wxBLUE_BRUSH);
    gc->SetPen(wxPen(*wxWHITE,1));
    gc->SetFont(*wxNORMAL_FONT,*wxWHITE);

    vector<wxObject *>::const_iterator i;
    for(i = layer.objects.begin(); i != layer.objects.end(); ++i) {

      MuniDrawCircle *circle = dynamic_cast<MuniDrawCircle *>(*i);
      if( circle ) {
	float d = 2.0*circle->r*zoom;
	float x = xoff + zoom*(circle->x - circle->r);
	float y = yoff + (height - zoom*(circle->y + circle->r));
	gc->DrawEllipse(x,y,d,d);
      }
	
      MuniDrawLine *line = dynamic_cast<MuniDrawLine *>(*i);
      if( line ) {
	wxPoint2DDouble points[2];
	points[0] = wxPoint2DDouble(xoff+zoom*line->x1,yoff+height-zoom*line->y1);
	points[1] = wxPoint2DDouble(xoff+zoom*line->x2,yoff+height-zoom*line->y2);
	gc->DrawLines(2,points);
      }

      MuniDrawText *text = dynamic_cast<MuniDrawText *>(*i);
      if( text ) {
	gc->DrawText(text->text,xoff+zoom*text->x,yoff+height-zoom*text->y);
      }
	
    }

    gc->EndLayer();
    delete gc;
  }  
 

  //  wxLogDebug("Drawing took: %f sec",sw.Time()/1000.0);

}

// void MuniDisplayCanvas::DrawSVG(wxPaintDC& dc, const wxXmlDocument& svg)
// {
//   if( !(svg.IsOk() && svg.GetRoot()->GetName() == "svg") ) return;

//   wxGraphicsContext *gc = wxGraphicsContext::Create(dc);
//   if( gc ) {
    
//     // default colors
//     gc->SetBrush(wxBrush(*wxBLUE));
//     gc->SetPen(wxPen(*wxYELLOW,1));
//     gc->SetFont(*wxNORMAL_FONT,*wxWHITE);

//     wxXmlNode *child = svg.GetRoot()->GetChildren();
//     while (child) {

//       if (child->GetName() == "line") {

// 	double x1,y1,x2,y2;

// 	wxXmlAttribute *prop = child->GetAttributes();
// 	while(prop) {
	    
// 	  wxString a = prop->GetValue();
// 	  double x;
// 	  a.ToDouble(&x);

// 	  if( prop->GetName() == "x1" )
// 	    x1 = x;
// 	  else if( prop->GetName() == "y1" )
// 	    y1 = x;
// 	  else if( prop->GetName() == "x2" )
// 	    x2 = x;
// 	  else if( prop->GetName() == "y2" )
// 	    y2 = x;

// 	  prop = prop->GetNext();
// 	}
// 	gc->StrokeLine(x1,y1,x2,y2);
	 
//       }

//       else if( child->GetName() == "text") {

// 	double x,y;
// 	wxString text(child->GetNodeContent());

// 	wxXmlAttribute *prop = child->GetAttributes();
// 	while(prop) {

// 	  wxString a = prop->GetValue();
// 	  double t;
// 	  a.ToDouble(&t);

// 	  if( prop->GetName() == "x" )
// 	    x = t;
// 	  else if( prop->GetName() == "y" )
// 	    y = t;

// 	  prop = prop->GetNext();
// 	}	  

// 	gc->DrawText(text,x,y);
//       }

//       else if( child->GetName() == "circle") {

// 	double cx,cy,r;

// 	wxXmlAttribute *prop = child->GetAttributes();
// 	while(prop) {

// 	  wxString a = prop->GetValue();
// 	  double x;
// 	  a.ToDouble(&x);

// 	  if( prop->GetName() == "cx" )
// 	    cx = x;
// 	  else if( prop->GetName() == "cy" )
// 	    cy = x;
// 	  else if( prop->GetName() == "r" )
// 	    r = x;

// 	  prop = prop->GetNext();
// 	}	  

// 	gc->DrawEllipse(cx-r/2.0,cy-r/2.0,r,r);
//       }
//       child = child->GetNext();
//     }
//     delete gc;
//   }

// }

void MuniDisplayCanvas::UpdateBitmap(int i, int j, const wxBitmap& bmp)
{
  wxASSERT(bitmap.IsOk() && bmp.IsOk());
  wxMemoryDC mdc(bitmap);
  mdc.DrawBitmap(bmp,i,j,false);

  if( memdc.IsOk() )
    memdc.DrawBitmap(bmp,xoff+i,yoff+j,false);
}


void MuniDisplayCanvas::OnMouseMotion(wxMouseEvent& event)
{
  wxClientDC dc(this);
  DoPrepareDC(dc);

  if( astrometry ) {

    return;
  }


  if( event.Moving() ) {

    wxPoint cursor = event.GetLogicalPosition(dc) - wxPoint(xoff,yoff);
    MuniSlewEvent evt(xEVT_SLEW);
    evt.i_x = cursor.x/zoom;
    evt.i_y = (bitmap.GetHeight() - cursor.y)/zoom;
    evt.z_x = cursor.x;
    evt.z_y = bitmap.GetHeight() - cursor.y;

    int size = config->detail_zoom;
    
    int x = evt.i_x;
    int y = evt.i_y;
    
    int x1 = x - size/2;
    int y1 = y - size/2;
    int x2 = x + size/2;
    int y2 = y + size/2;

    if( x1 > 0 && y1 > 0 && x2 < fitsimage.GetWidth() && y2 < fitsimage.GetHeight() ){
      evt.picture = display.GetImage(fitsimage.GetSubImage(x1,y1,size,size));
    }

    wxQueueEvent(handler,evt.Clone());
  }
  else if( event.Dragging() ) {

    if( ! dragging ) {
    
      SetCursor(wxCursor(wxCURSOR_HAND));
      xdrag0 = event.GetLogicalPosition(dc).x;
      ydrag0 = event.GetLogicalPosition(dc).y;
      dragging = true;

    }
    else if( dragging ) {

      int dx,dy,i0,j0;
      GetScrollPixelsPerUnit(&dx,&dy);
      GetViewStart(&i0,&j0);
      wxASSERT(dx > 0 && dy > 0);
      int i = i0 - (event.GetLogicalPosition(dc).x - xdrag0) / dx;
      int j = j0 - (event.GetLogicalPosition(dc).y - ydrag0) / dy;
      Scroll(i,j);

    }

  }

}

void MuniDisplayCanvas::OnMouseWheel(wxMouseEvent& event)
{
  int id = event.GetWheelRotation() > 0 ? wxID_ZOOM_IN : wxID_ZOOM_OUT ;
  wxQueueEvent(topwin,new wxCommandEvent(wxEVT_COMMAND_MENU_SELECTED,id));
}

void MuniDisplayCanvas::OnLeftUp(wxMouseEvent& event)
{
  if( dragging ) {
    SetCursor(*wxSTANDARD_CURSOR);
    dragging = false;
  }
}

void MuniDisplayCanvas::OnStick(wxCommandEvent& event)
{
  stick = event.IsChecked();
}

void MuniDisplayCanvas::OnMenu(wxMouseEvent& WXUNUSED(event))
{
  wxTopLevelWindow *twin = static_cast<wxTopLevelWindow *>(topwin);
  if( !twin ) return;

  wxMenu popup;

  if( twin->IsFullScreen() ) {
    popup.Append(ID_MENU_FULLSCREEN,"Leave Fullscreen");
    popup.AppendSeparator();
    popup.Append(wxID_ZOOM_100,"Normal size\tCtrl+0");
    popup.Append(wxID_ZOOM_IN,"Zoom\tCtrl++");
    popup.Append(wxID_ZOOM_OUT,"Shrink\tCtrl+-");
    popup.Append(wxID_ZOOM_FIT,"Best fit\tCtrl+*");
    popup.Append(ID_CYCLE_ITT,"Cycle Itt\tCtrl+T");
    popup.Append(ID_CYCLE_PAL,"Cycle Palette\tCtrl+P");
  }
  else /*if( ! twin->IsFullScreen() )*/ {
    popup.AppendCheckItem(ID_STICK,"Stick"); // Ctrl+S does not works
    popup.Check(ID_STICK,stick);
  }

  PopupMenu(&popup);
}


void MuniDisplayCanvas::OnKeyDown(wxKeyEvent& event)
{
  long keycode = event.GetKeyCode();

  wxTopLevelWindow *twin = static_cast<wxTopLevelWindow *>(topwin);

  if( twin && twin->IsFullScreen() && keycode == WXK_ESCAPE ){
    wxQueueEvent(topwin,new wxCommandEvent(wxEVT_COMMAND_MENU_SELECTED,ID_FULLSCREEN));
    return;
  }

  if( keycode == WXK_ESCAPE ) {
    StopRendering(); 
    return;
  }

  if( event.GetModifiers() & wxMOD_CMD ) {
    int id = wxID_ANY;
    if( keycode == WXK_NUMPAD_MULTIPLY )
      id = wxID_ZOOM_FIT;
    else if( ! fit ) {
      switch(keycode) {
      case WXK_NUMPAD_ADD:      id = wxID_ZOOM_IN;  break;
      case WXK_NUMPAD_SUBTRACT: id = wxID_ZOOM_OUT; break;
      case WXK_NUMPAD0:         id = wxID_ZOOM_100; break;
      case WXK_NUMPAD_MULTIPLY: id = wxID_ZOOM_FIT; break;
      case WXK_LEFT:            id = wxID_BACKWARD; break;
      case WXK_RIGHT:           id = wxID_FORWARD;  break;
      }
    }
      
    if( id != wxID_ANY ) {
      wxQueueEvent(topwin,new wxCommandEvent(wxEVT_COMMAND_MENU_SELECTED,id));
      return;
    }
  }

  long x = event.GetX();
  long y = event.GetY();

  switch(keycode) {
  case WXK_LEFT: 
    x--; break;
  case WXK_UP: 
    y--; break;
  case WXK_RIGHT:
    x++; break;
  case WXK_DOWN:
    y++; break;
  }
  WarpPointer(x,y);

  event.Skip();
}

void MuniDisplayCanvas::OnTuneFine(MuniTuneEvent& e)
{
  int oldzoom = zoom;

  switch(e.GetId()) {
  case ID_ITT_TYPE:   itt.SetItt(e.GetString()); break;
  case ID_ITT_OFFSET: itt.SetBlack(e.x); break;
    //  case ID_ITT_SLOPE:  itt.SetSensitivity(e.x); break;
  case ID_ITT_BRIGHTNESS: itt.SetBrightness(e.x); break;
  case ID_ITT_CONTRAST:  itt.SetContrast(e.x); break;
  case ID_ITT_AMP:    itt.SetAmp(e.x); break;
  case ID_ITT_ZERO:   itt.SetZero(e.x); break;

  case ID_COLOR_NVISION: color.SetNightVision(e.GetInt()); break;
  case ID_COLOR_SATUR:   color.SetSaturation(e.x); break;
  case ID_COLOR_HUE:     color.SetHue(e.x); break;
  case ID_COLOR_WHITEU:  color.SetWhitePoint(e.x,-1.0); break;
  case ID_COLOR_WHITEV:  color.SetWhitePoint(-1.0,e.x); break;
  case ID_COLOR_NTHRESH: color.SetNightThresh(e.x); break;
  case ID_COLOR_NWIDTH:  color.SetNightWidth(e.x); break;
  case ID_COLOR_MEAN:    color.SetLevel(e.index,e.x); break;
  case ID_COLOR_WEIGHT:  color.SetWeight(e.index,e.x); break;

  case ID_PALETTE_TYPE:     pal.SetPalette(e.GetString()); break;
  case ID_PALETTE_NEGATIVE: pal.SetNegative(e.GetInt()); break;

  case ID_ZOOM_SCALE: zoom = e.x > 0.0 ? e.x : GetFitZoom(); fit = e.fit; break;

  case ID_RESET: Reset(); break;

  default: wxLogDebug("WARNING: MuniDisplay::OnTuneFine - unknown ID");
  }

  if( e.GetId() == ID_ZOOM_SCALE ) {
    scale = zoom;

    int w = int(zoom*hdu.Width());
    int h = int(zoom*hdu.Height());
    if( w < 1 ) w = 1;
    if( h < 1 ) h = 1;

    if( bitmap.IsOk() && (oldzoom < 1.0 || zoom < 1.0) ) {

      zooming = true;
      if( zoom > 1.0 )
	zoom = 1.0;

      int w = wxMax(int(zoom*hdu.Width()),1);
      int h = wxMax(int(zoom*hdu.Height()),1);

      wxImage img(bitmap.ConvertToImage());
      img.Rescale(w,h);
      bitmap = wxBitmap(img);
    }

    int wc,hc;
    GetClientSize(&wc,&hc);

    w = wxMax(w,wc);
    h = wxMax(h,hc);

    SetVirtualSize(w,h);
    InitDC(w,h,bitmap);

    if( ! zooming ) {
      Refresh();
      return;
    }
  }

  Refresh();
  rendering = true;
}

void MuniDisplayCanvas::OnAstrometry(MuniAstrometryEvent& e)
{
  astrometry = e.astrometry;
  layers.clear();

  if( astrometry ) {

    //  wxLogDebug(e.proj+" %f %f %f",e.xcen,e.ycen,e.scale);

    aangle = e.angle;
    ascale = e.scale;
    acen = e.acen;
    dcen = e.dcen;
    xcen = e.xcen;
    ycen = e.ycen;
    //  layers.push_back(e.svg);
    layers.push_back(e.layer);
    //  wxLogDebug("%d",(int)e.layer.objects.size());
  }

  Refresh();
}

void MuniDisplayCanvas::OnConfigUpdated(wxCommandEvent& event)
{
  display.SetTemperature(config->color_temperature);
  display.SetColorspace(config->display_colorspace);
  color.SetWhitePoint(config->color_uwhitepoint,config->color_vwhitepoint);
  rendering = true;
}

double MuniDisplayCanvas::GetFitZoom() const
{
  if( ! hdu.IsOk() ) return 1.0;

  wxSize isize(hdu.Width(),hdu.Height());
  wxSize csize = GetSize();

  if( isize.GetWidth() > 0 && isize.GetHeight() > 0 ) {
    double sx = double(csize.GetWidth())/double(isize.GetWidth());
    double sy = double(csize.GetHeight())/double(isize.GetHeight());
    return  sx > sy ? sy : sx;
  }
  else if( isize.GetWidth() > 0 && isize.GetHeight() == 0 ) {
    double sx = double(csize.GetWidth())/double(isize.GetWidth());
    return  sx;
  }

  return 1.0;
}


void MuniDisplayCanvas::SetInitZoom(double z)
{
  if( z > 0.0 )
    zoom = z;
  else {
    double zz = GetFitZoom();
    zoom = zz < 1.0 ? zz : 1.0;
  }
  zooming = true;
}

/*
void MuniDisplayCanvas::OverlayBitmap(const vector<MuniDrawBase *>& drawobj)
{
  wxASSERT(bitmap.IsOk());

  wxBitmap layer = wxBitmap(bitmap.GetWidth(),bitmap.GetHeight());

  wxMemoryDC mdc(layer);
  mdc.SetBackground(*wxBLACK_BRUSH);
  mdc.Clear();
  int height = layer.GetHeight();

  wxGraphicsContext *gc = wxGraphicsContext::Create(mdc);
  if( gc ) {
    
    gc->SetBrush(wxBrush(*wxBLUE));
    gc->SetPen(wxPen(*wxYELLOW,1));

    vector<MuniDrawBase *>::const_iterator i;
    for(i = drawobj.begin(); i != drawobj.end(); ++i) {

      MuniDrawCircle *e = static_cast<MuniDrawCircle *>(*i);
      if( e ) {
	float r = e->r;
	float x = e->cx - r/2.0;
	float y = height - (e->cy + r/2.0);
	gc->DrawEllipse(x,y,r,r);
      }

    }
    delete gc;
  }
  mdc.SelectObject(wxNullBitmap);

  wxImage i = layer.ConvertToImage();
  i.SetMaskColour(0,0,0);
  
  overlay.push_back(wxBitmap(i));
  
}
*/

// void MuniDisplayCanvas::OverlayGrid(const vector<wxGraphicsPath>& paths)
// {
//   wxASSERT(bitmap.IsOk());

//   wxBitmap layer = wxBitmap(bitmap.GetWidth(),bitmap.GetHeight());

//   wxMemoryDC mdc(layer);
//   mdc.SetBackground(*wxBLACK_BRUSH);
//   mdc.Clear();
//   int height = layer.GetHeight();

//   wxGraphicsContext *gc = wxGraphicsContext::Create(mdc);
//   if( gc ) {
    
//     gc->SetBrush(wxBrush(*wxBLUE));
//     gc->SetPen(wxPen(*wxYELLOW,1,wxPENSTYLE_SOLID));

//     wxGraphicsPath path = gc->CreatePath();

//     path.MoveToPoint(333,333);
//     path.AddLineToPoint(500,500);

//     /*
//     vector<wxGraphicsPath>::const_iterator i;
//     for(i = paths.begin(); i != paths.end(); ++i) {
//     */      

//     gc->StrokePath(path);

//   }
//   delete gc;
//   mdc.SelectObject(wxNullBitmap);

//   wxImage i = layer.ConvertToImage();
//   i.SetMaskColour(0,0,0);
  
//   overlay.push_back(wxBitmap(i));
  
// }


// -- MuniDisplay

MuniDisplay::MuniDisplay(wxWindow *w, MuniConfig *c):
  wxWindow(w,wxID_ANY),config(c),panel(0),canvas(0),tune(0),astrometry(0)//,photometry(0)
{
  wxASSERT(config);

  canvas = new MuniDisplayCanvas(this,c);
  panel = new MuniDisplayPanel(this,c);

  wxASSERT(canvas && panel);

  static_cast<MuniDisplayCanvas *>(canvas)->SetHandler(panel);

  wxBoxSizer *topsizer = new wxBoxSizer(wxHORIZONTAL);
  topsizer->Add(canvas,wxSizerFlags(1).Expand());
  topsizer->Add(panel,wxSizerFlags().Expand().Border(wxLEFT|wxRIGHT));
  SetSizerAndFit(topsizer);
  
  Bind(wxEVT_ENTER_WINDOW,&MuniDisplay::OnEnterWin,this);
  //  Bind(wxEVT_LEAVE_WINDOW,&MuniDisplay::OnLeaveWin,this);
  Bind(wxEVT_COMMAND_MENU_SELECTED,&MuniDisplay::OnLeaveFullscreen,this,
       ID_MENU_FULLSCREEN);
  Bind(EVT_TUNE,&MuniDisplayCanvas::OnTuneFine,
       static_cast<MuniDisplayCanvas *>(canvas));
  Bind(EVT_ASTROMETRY,&MuniDisplayCanvas::OnAstrometry,
       static_cast<MuniDisplayCanvas *>(canvas));
  Bind(EVT_CONFIG_UPDATED,&MuniDisplayCanvas::OnConfigUpdated,
       static_cast<MuniDisplayCanvas *>(canvas));
}

bool MuniDisplay::SetHdu(const FitsHdu& hdu, const wxImage& icon)
{
  array = FitsArray(hdu);
  thumb = icon;

  // histogram + itt
  FitsArrayStat stat(array.IsColor() ? array.Plane(1) : array);
  double xmin = stat.Med() - 5.0*stat.Mad();
  double xmax = stat.Med() + 7.0*stat.Mad();
  hist = FitsHisto(array,xmin,xmax);

  wxASSERT(array.IsOk() && icon.IsOk());
  if( ! (array.IsOk() && icon.IsOk()) ) return false;

  wxASSERT(static_cast<MuniDisplayCanvas *>(canvas));
  wxASSERT(static_cast<MuniDisplayPanel *>(panel));

  static_cast<MuniDisplayPanel *>(panel)->SetHdu(array,hist);
  static_cast<MuniDisplayCanvas *>(canvas)->SetHdu(array,icon,stat);

  return true;
}


bool MuniDisplay::IsRendering() const
{
  wxASSERT(static_cast<MuniDisplayCanvas *>(canvas));
  return static_cast<MuniDisplayCanvas *>(canvas)->IsRendering();
}


double MuniDisplay::GetZoom() const
{
  wxASSERT(static_cast<MuniDisplayCanvas *>(canvas));
  return static_cast<MuniDisplayCanvas *>(canvas)->GetZoom();
}

FitsItt MuniDisplay::GetItt() const
{
  wxASSERT(static_cast<MuniDisplayCanvas *>(canvas));
  return static_cast<MuniDisplayCanvas *>(canvas)->GetItt();
}

FitsColor MuniDisplay::GetColor() const
{
  wxASSERT(static_cast<MuniDisplayCanvas *>(canvas));
  return static_cast<MuniDisplayCanvas *>(canvas)->GetColor();
}

FitsPalette MuniDisplay::GetPalette() const
{
  wxASSERT(static_cast<MuniDisplayCanvas *>(canvas));
  return static_cast<MuniDisplayCanvas *>(canvas)->GetPalette();
}

wxImage MuniDisplay::GetImage()
{
  wxASSERT(static_cast<MuniDisplayCanvas *>(canvas));
  return static_cast<MuniDisplayCanvas *>(canvas)->GetImage();
}

bool MuniDisplay::GetFit() const
{
  wxASSERT(static_cast<MuniDisplayCanvas *>(canvas));
  return static_cast<MuniDisplayCanvas *>(canvas)->GetFit();
}

void MuniDisplay::ShowPanel(bool show)
{
  wxASSERT(panel);
  panel->Show(show);
  Layout();
}

void MuniDisplay::OnLeaveFullscreen(wxCommandEvent& event)
{
  wxCommandEvent e(wxEVT_COMMAND_MENU_SELECTED,ID_FULLSCREEN);
  wxQueueEvent(GetParent(),e.Clone());
}

void MuniDisplay::OnEnterWin(wxMouseEvent& event)
{
  canvas->SetFocus();
  //  wxQueueEvent(GetParent(),event.Clone());
}

void MuniDisplay::OnLeaveWin(wxMouseEvent& event)
{
  wxQueueEvent(GetParent(),event.Clone());
}

double MuniDisplay::GetFitZoom() const
{
  wxASSERT(canvas);
  return static_cast<MuniDisplayCanvas *>(canvas)->GetFitZoom();
}

/*
void MuniDisplay::SetOverlay(wxInputStream& istream)
{
  wxASSERT(canvas);

  MuniSvg svg(istream);
  static_cast<MuniDisplayCanvas *>(canvas)->SetOverlay(svg.GetDrawObjects());
}
*/

void MuniDisplay::SetStars(FitsTable& table)
{
  wxASSERT(canvas);

  wxMemoryOutputStream ostream;
  table.GetStarChart(ostream);
      
  wxMemoryInputStream istream(ostream);
  //  MuniSvg svg(istream);
  //  static_cast<MuniDisplayCanvas *>(canvas)->AddOverlay(svg.GetDrawObjects());
}

/*
vector<MuniDrawBase *> MuniSvg::GetDrawObjects() const
{
  vector<MuniDrawBase *> objects;

  const wxXmlNode *node = GetRoot()->GetChildren();
  while(node) {

    if( node->GetName() == "circle" ) {

      double x;
      float cx=0.0,cy=0.0,r=0.0;
      wxString a;
      wxXmlAttribute *prop = node->GetAttributes();
      while(prop) {

	if( prop->GetName() == "cx" ) {
	  a = prop->GetValue();
	  if( a.ToDouble(&x) )
	    cx = x;
	}
	else if( prop->GetName() == "cy" ) {
	  a = prop->GetValue();
	  if( a.ToDouble(&x) )
	    cy = x;
	}
	else if( prop->GetName() == "r" ) {
	  a = prop->GetValue();
	  if( a.ToDouble(&x) )
	    r = x;
	}
	
	prop = prop->GetNext();
      }

      objects.push_back(new MuniDrawCircle(cx,cy,r));
    }

    node = node->GetNext();
  }

  return objects;
}
*/

void MuniDisplay::ShowGrid(bool show)
{
  wxASSERT(canvas);
  vector<wxGraphicsPath> paths;

  /*
  wxGraphicsPath p;
  p.MoveToPoint(333,333);
  p.AddLineToPoint(500,500);
  paths.push_back(p);
  */

  //  static_cast<MuniDisplayCanvas *>(canvas)->ShowGrid(paths);
}

void MuniDisplay::ShowTune(bool show)
{
  if( ! tune ) {

    if( array.IsColor() )
      tune = new MuniTune(this,ID_TUNE,GetPosition(),wxDefaultSize,config,array,GetItt(),GetColor());
    else 
      tune = new MuniTune(this,ID_TUNE,GetPosition(),wxDefaultSize,config,array,GetItt(),GetPalette());

    tune->Show(true);

    Bind(wxEVT_CLOSE_WINDOW,&MuniDisplay::OnCloseTune,this,tune->GetId());
  }
}


void MuniDisplay::OnCloseTune(wxCloseEvent& event)
{
  Unbind(wxEVT_CLOSE_WINDOW,&MuniDisplay::OnCloseTune,this,tune->GetId());
  tune->Destroy();
  tune = 0;
}

void MuniDisplay::ShowPhotometry(const wxString& file)
{
  MuniPhotometry photometry(this,config,file);
  if( photometry.ShowModal() == wxID_OK ) {

    wxString file = photometry.GetResult();
    
    FitsFile fits(file);

    if( fits.IsOk() )
      for(size_t i = 0; i < fits.HduCount(); i++)
	if( fits.Hdu(i).GetKey("EXTNAME").Find("MUNIPACK") != wxNOT_FOUND ) {
	  FitsTable t(fits.Hdu(i));
	  SetStars(t);
	  /*      
	  wxMemoryOutputStream ostream;
	  t.GetStarChart(ostream);
      
	  wxMemoryInputStream istream(ostream);
	  MuniSvg svg(istream);
	  static_cast<MuniDisplayCanvas *>(canvas)->SetOverlay(svg.GetDrawObjects());
	  */
	  //	  SetOverlay(istream);
	}

    wxRemoveFile(file);
  }
  //  return;

  /*
  if( ! photometry ) {
    photometry = new MuniPhotometry(this,config,file);
    photometry->ShowModal();
    Bind(wxEVT_CLOSE_WINDOW,&MuniDisplay::OnClosePhotometry,this,photometry->GetId());
  }
  */
}

void MuniDisplay::Calibrate(const wxString& file)
{
  // check availability of write access
  wxFileName fn(file);
  if( ! fn.IsFileWritable() ) {
    wxLogWarning("File `"+file+"' is not writtable. Results of calibration should not be saved.");
  }

  MuniCalibrate calibrate(this,config,file);
  if( calibrate.ShowModal() == wxID_OK ) {

    /*
    wxString cfile = calibrate.GetResult();
    
    FitsFile fits(cfile);

    if( fits.IsOk() )
      for(size_t i = 0; i < fits.HduCount(); i++)
	if( fits.Hdu(i).GetKey("EXTNAME").Find("MUNIPACK") != wxNOT_FOUND ) {
	  FitsTable t(fits.Hdu(i));
	  SetStars(t);
	}

    wxRemoveFile(cfile);
    */

    wxCommandEvent e(EVT_FILELOAD,GetId());
    e.SetEventObject(GetParent());
    e.SetString(file);
    wxQueueEvent(GetParent(),e.Clone());


  }
}

void MuniDisplay::Astrometry(const wxString& file, const FitsTable& t)
{
  if( astrometry ) return;

  astrometry = new MuniAstrometry(this,config);
  astrometry->SetFile(file);
  astrometry->SetArray(array);
  astrometry->SetDetectedSources(t);
  astrometry->SetOutput(static_cast<MuniView *>(GetParent())->CreateTempFileName());
  astrometry->Show();

  Bind(wxEVT_CLOSE_WINDOW,&MuniDisplay::OnCloseAstrometry,this,astrometry->GetId());

}

void MuniDisplay::OnCloseAstrometry(wxCloseEvent& event)
{
  int code = astrometry->GetReturnCode();
  wxLogDebug("Return code= %d",code);

  if( astrometry->GetReturnCode() == wxID_APPLY ) {

    //    wxArrayString com(astrometry->GetResults());
    // GetPar........................................
    //    static_cast<MuniView *>(GetParent())->SetAstrometry();
    static_cast<MuniView *>(GetParent())->ReloadFile();
  }
  else
    static_cast<MuniView *>(GetParent())->UnlinkBackup();


  Unbind(wxEVT_CLOSE_WINDOW,&MuniDisplay::OnCloseAstrometry,this,astrometry->GetId());
  astrometry->Destroy();
  astrometry = 0;

  //  wxLogDebug("MuniDisplay::OnCloseAstrometry: %d",(int)com.GetCount());
}

// void MuniDisplay::OnClosePhotometry(wxCloseEvent& event)
// {
//   wxLogDebug("MuniDisplay::OnClosePhotometry");
//   Unbind(wxEVT_CLOSE_WINDOW,&MuniDisplay::OnClosePhotometry,this,photometry->GetId());
//   //  photometry->Destroy();
//   photometry = 0;
// }

// ---- ImageRender ---------------------------------

ImageRender::ImageRender(wxEvtHandler *eh, const FitsImage& fi, 
			 const FitsImage& fis, const FitsDisplay& d, 
			 double zz, bool z):
  wxThread(wxTHREAD_DETACHED),handler(eh),fitsimage(fi), fimage(fis), 
  display(d), zoom(zz), zooming(z)
{
  wxASSERT(handler && fitsimage.IsOk());
}

ImageRender::~ImageRender()
{
  wxCriticalSectionLocker enter(static_cast<MuniDisplayCanvas *>(handler)->renderCS);
  static_cast<MuniDisplayCanvas *>(handler)->render = 0;
}

wxThread::ExitCode ImageRender::Entry()
{
  bool test_destroy = false;

#ifdef __WXDEBUG__
  wxStopWatch sw;
#endif  

  // length of square side
  // This value is choosed to be the duration of a single iteration
  // shorter than an user reaction time (~0.1 sec). Note that
  // short sides implicates frequent generation of events and memory
  // allocations. So the value must by determined carefully.
  const int side = 3*137;

  FitsImage newimage;

  if( fimage.IsOk() && ! zooming )
    newimage = fimage;

  else {
    fimage = fitsimage;
    newimage = fitsimage;
    newimage.Rescale(zoom);
  }
    

  //  wxLogDebug("Init took: %d msec",(int)sw.Time());

  int w = fimage.GetWidth();
  int h = fimage.GetHeight();

  int dx = 0, dy = 0;

  int x = 0;
  int y = 0;

  for(int j = 0; j < h ; j += side) {

    x = 0;

    for(int i = 0; i < w; i += side) {

      //      wxLogDebug("Piece rendering took: %d msec",(int)sw.Time());

      if( TestDestroy() ) {
	test_destroy = true;
	wxLogDebug("Rendering took when delete: %f sec",sw.Time()/1000.0);
	break;
      }

      wxASSERT(w > i && h > j);

      dx = wxMin(w - i,side);
      dy = wxMin(h - j,side);

      wxASSERT(dx >= 0 && dy >= 0);

      FitsImage sub(fimage.GetSubImage(i,j,dx,dy));
      
      if( zooming ) {
	FitsImage z(sub.Scale(zoom));
	sub = z;
	//	wxASSERT(dx == sub.GetWidth() && dy == sub.GetHeight());
	dx = sub.GetWidth();
	dy = sub.GetHeight();
      }

      newimage.SetSubImage(x,y,sub);
      
      wxASSERT(sub.IsOk());

      MuniRenderEvent ev(EVT_RENDER,ID_SUBRENDER);
      ev.picture = display.GetImage(sub);
      ev.x = x;
      ev.y = newimage.GetHeight() - y - dy;
      wxQueueEvent(handler,ev.Clone());

      wxASSERT(dx > 0);
      x = x + dx;
    }
    wxASSERT(dy > 0);
    y = y + dy;
  }

  MuniRenderEvent ev(EVT_RENDER,ID_RENDER);
  if( test_destroy )
    ev.image = FitsImage();
  else
    ev.image = FitsImage(newimage);
  wxQueueEvent(handler,ev.Clone());

  wxLogDebug("Rendering took: %f sec",sw.Time()/1000.0);
  return (wxThread::ExitCode) 0;
}



/*

   HOW to create a throbbler

   - download glunarclock source package:
        http://glunarclock.sourceforge.net/

   - separate the long image sequence and resize images to single exposures:

       for A in `seq 0 55`; do 
          P=$((A*48)); 
          X=$(printf "%02d" $A); 
          convert moon_56frames.png -crop 48x48+$P+1 -resize 32x32 m_$X.png;
       done

   - animate:
     
       convert -delay 10  +repage m_*.png throbbler.gif


*/
     


